/*
 * OREKIT-X
 * Copyright 2002-2008 CS Communication & Systemes
 * 
 * Parts of this software package have been licensed to CS
 * Communication & Systemes (CS) under one or more contributor license
 * agreements.  See the NOTICE file distributed with this work for
 * additional information.
 *  
 * This is an experimental copy of OREKIT from www.orekit.org.
 * Please use the original OREKIT from orekit.org for normal work
 * unrelated to this research project.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you
 * may not use this file except in compliance with the License.  You
 * may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied.  See the License for the specific language governing
 * permissions and limitations under the License.
 */
package ore.io;

import ore.PositionVelocityChebyshev;
import ore.bodies.JPLConstants;
import ore.bodies.JPLEphemerides;
import ore.io.DataProvidersManager;
import ore.errors.OrekitException;
import ore.time.AbsoluteDate;
import ore.time.ChronologicalComparator;
import ore.time.TimeScalesFactory;
import ore.time.TimeStamped;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.text.ParseException;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;

/** Loader for JPL ephemerides binary files (DE 405, DE 406).
 * <p>JPL ephemerides binary files contain ephemerides for all solar system planets.</p>
 * <p>The JPL ephemerides binary files are recognized thanks to their base names,
 * which must match the pattern <code>unx[mp]####.ddd</code> (or
 * <code>unx[mp]####.ddd.gz</code> for gzip-compressed files) where # stands for a
 * digit character and where ddd is an ephemeris type (typically 405 or 406).</p>
 * @author Luc Maisonobe
 * @version $Revision:1665 $ $Date:2008-06-11 12:12:59 +0200 (mer., 11 juin 2008) $
 */
public class JPLEphemeridesLoader
    extends ore.bodies.JPLEphemerides
    implements DataLoader
{

    /** Get astronomical unit.
     * @return astronomical unit in meters
     * @exception OrekitException if constants cannot be loaded
     */
    public static double getAstronomicalUnit() throws OrekitException {

        if (HasNotJPLConstants()) {
            LoadConstants();
        }
        return 1000.0 * GetConstant(Constant.AU);
    }

    /** Get Earth/Moon mass ratio.
     * @return Earth/Moon mass ratio
     * @exception OrekitException if constants cannot be loaded
     */
    public static double getEarthMoonMassRatio() throws OrekitException {

        if (HasNotJPLConstants()) {
            LoadConstants();
        }
        return GetConstant(Constant.EMRAT);
    }

    /** Get the gravitational coefficient of a body.
     * @param body body for which the gravitational coefficient is requested
     * @return gravitational coefficient in m<sup>3</sup>/s<sup>2</sup>
     * @exception OrekitException if constants cannot be loaded
     */
    public static double getGravitationalCoefficient(final EphemerisType body)
        throws OrekitException
    {
        if (HasNotJPLConstants()) {
            LoadConstants();
        }
        return GetGravitationalCoefficient(body);
    }

    /** Get a constant defined in the ephemerides headers.
     * <p>Note that since constants are defined in the JPL headers
     * files, they are available as soon as one file is available, even
     * if it doesn't match the desired central date. This is because the
     * header must be parsed before the dates can be checked.</p>
     * @param name name of the constant
     * @return value of the constant of NaN if the constant is not defined
     * @exception OrekitException if constants cannot be loaded
     */
    public static double getConstant(final String name) throws OrekitException {

        if (HasNotJPLConstants()) {
            LoadConstants();
        }
        return GetConstant(name);
    }

    /** Load the header constants.
     * @exception OrekitException if constants cannot be loaded
     */
    private static void LoadConstants() throws OrekitException {
        if (!DataProvidersManager.getInstance().feed(SUPPORTED_FILES, new JPLConstantsLoader())) {
            throw new OrekitException(NO_JPL_FILES_FOUND);
        }
    }

    /** Error message for no JPL files. */
    private static final String NO_JPL_FILES_FOUND =
        "no JPL ephemerides binary files found";

    /** Error message for header read error. */
    protected static final String HEADER_READ_ERROR =
        "unable to read header record from JPL ephemerides binary file {0}";

    /** Error message for unsupported file. */
    private static final String NOT_JPL_EPHEMERIS =
        "file {0} is not a JPL ephemerides binary file";

    /** Error message for unsupported file. */
    private static final String OUT_OF_RANGE_DATE =
        "out of range date for ephemerides: {0}, [{1}, {2}]";

    /** Binary record size in bytes for DE 405. */
    private static final int DE405_RECORD_SIZE = 1018 * 8;

    /** Binary record size in bytes for DE 406. */
    private static final int DE406_RECORD_SIZE =  728 * 8;

    /** Supported files name pattern. */
    private static final String SUPPORTED_FILES = "^unx[mp](\\d\\d\\d\\d)\\.(?:(?:405)|(?:406))$";

    /** Index of the first data for selected body. */
    private int firstIndex;

    /** Number of coefficients for selected body. */
    private int coeffs;

    /** Number of chunks for the selected body. */
    private int chunks;


    /** 
     * Load the JPL ephemerides binary files.
     * @param type ephemeris type to load
     * @param centralDate desired central date
     * (all data within a +/-50 days range around this date will be loaded)
     * @exception OrekitException if the header constants cannot be read
     */
    public JPLEphemeridesLoader(final EphemerisType type, final AbsoluteDate centralDate)
        throws OrekitException
    {
        super(type,centralDate);
        LoadConstants();
        Instance = this;
        if (!DataProvidersManager.getInstance().feed(SUPPORTED_FILES, this)) {
            throw new OrekitException(NO_JPL_FILES_FOUND);
        }
    }


    /** {@inheritDoc}
     */
    public boolean stillAcceptsData() {
        return true;
    }
    /** {@inheritDoc}
     */
    public void loadData(final InputStream input, final String name)
        throws OrekitException, IOException
    {
        final byte[] first = JPLEphemeridesLoader.ReadFirstRecord(input, name);
        final byte[] second = new byte[first.length];

        this.parseFirstHeaderRecord(first, name);

        if (this.tooFarRange(startEpoch, finalEpoch))
            return;

        else {
            if (!JPLEphemeridesLoader.ReadInRecord(input, second, 0))
                throw new OrekitException(HEADER_READ_ERROR, name);

            else {
                if ((!this.isSetDefined())&& JPLConstantsLoader.ParseConstants(this,first,second))
                    JPLConstants.Instance = this;

                JPLEphemerides.Instance = this;

                while (JPLEphemeridesLoader.ReadInRecord(input, second, 0)) {
                    this.parseDataRecord(second);
                }
            }
        }
    }

    /** Check if a range is too far from the central date.
     * <p>"Too far" is considered to be either end more than one year
     * before central date or to start more than one year after central
     * date.</p>
     * @param start start date of the range
     * @param end end date of the range
     * @return true if the range is closer than one year to the central date
     */
    private boolean tooFarRange(final AbsoluteDate start, final AbsoluteDate end) {

        // 50 days in seconds
        final double fiftyDays = 50 * 86400;

        // check range bounds
        return (centralDate.durationFrom(end) > fiftyDays) ||
               (start.durationFrom(centralDate) > fiftyDays);

    }

    /** Parse the first header record.
     * @param record first header record
     * @param name name of the file (or zip entry)
     * @exception OrekitException if the header is not a JPL ephemerides binary file header
     */
    private void parseFirstHeaderRecord(final byte[] record, final String name)
        throws OrekitException {

        // extract covered date range
        this.startEpoch = JPLEphemeridesLoader.ExtractDate(record, 2652);
        this.finalEpoch = JPLEphemeridesLoader.ExtractDate(record, 2660);
        boolean ok = finalEpoch.compareTo(startEpoch) > 0;

        // check astronomical unit consistency
        final double au = 1000 * JPLEphemeridesLoader.ExtractDouble(record, 2680);
        ok = ok && (au > 1.4e11) && (au < 1.6e11);
        if (Math.abs(getAstronomicalUnit() - au) >= 0.001) {
            throw new OrekitException("inconsistent values of astronomical unit in JPL ephemerides files: ({0} and {1})",
                                      getAstronomicalUnit(), au);
        }

        final double emRat = JPLEphemeridesLoader.ExtractDouble(record, 2688);
        ok = ok && (emRat > 80) && (emRat < 82);
        if (Math.abs(getEarthMoonMassRatio() - emRat) >= 1.0e-8) {
            throw new OrekitException("inconsistent values of Earth/Moon mass ratio in JPL ephemerides files: ({0} and {1})",
                                      getEarthMoonMassRatio(), emRat);
        }

        // indices of the Chebyshev coefficients for each ephemeris
        for (int i = 0; i < 12; ++i) {
            final int row1 = JPLEphemeridesLoader.ExtractInt(record, 2696 + 12 * i);
            final int row2 = JPLEphemeridesLoader.ExtractInt(record, 2700 + 12 * i);
            final int row3 = JPLEphemeridesLoader.ExtractInt(record, 2704 + 12 * i);
            ok = ok && (row1 > 0) && (row2 >= 0) && (row3 >= 0);

            if (((i ==  0) && (this.type == EphemerisType.MERCURY))    ||
                ((i ==  1) && (this.type == EphemerisType.VENUS))      ||
                ((i ==  2) && (this.type == EphemerisType.EARTH_MOON)) ||
                ((i ==  3) && (this.type == EphemerisType.MARS))       ||
                ((i ==  4) && (this.type == EphemerisType.JUPITER))    ||
                ((i ==  5) && (this.type == EphemerisType.SATURN))     ||
                ((i ==  6) && (this.type == EphemerisType.URANUS))     ||
                ((i ==  7) && (this.type == EphemerisType.NEPTUNE))    ||
                ((i ==  8) && (this.type == EphemerisType.PLUTO))      ||
                ((i ==  9) && (this.type == EphemerisType.MOON))       ||
                ((i == 10) && (this.type == EphemerisType.SUN)))
            {
                this.firstIndex = row1;
                this.coeffs     = row2;
                this.chunks     = row3;
            }
        }

        // compute chunks duration
        final double timeSpan = JPLEphemeridesLoader.ExtractDouble(record, 2668);
        ok = ok && (timeSpan > 0) && (timeSpan < 100);
        this.chunksDuration = 86400.0 * (timeSpan / chunks);
        if (Double.isNaN(this.maxChunksDuration)) {
            this.maxChunksDuration = this.chunksDuration;
        } else {
            this.maxChunksDuration = Math.max(this.maxChunksDuration, this.chunksDuration);
        }

        // sanity checks
        if (!ok) {
            throw new OrekitException(NOT_JPL_EPHEMERIS, name);
        }

    }

    /** Parse regular ephemeris record.
     * @param record record to parse
     * @exception OrekitException if the header is not a JPL ephemerides binary file header
     */
    private void parseDataRecord(final byte[] record) throws OrekitException {

        // extract time range covered by the record
        final AbsoluteDate rangeStart = JPLEphemeridesLoader.ExtractDate(record, 0);
        if (rangeStart.compareTo(startEpoch) < 0) {
            throw new OrekitException(OUT_OF_RANGE_DATE, rangeStart, startEpoch, finalEpoch);
        }

        final AbsoluteDate rangeEnd   = JPLEphemeridesLoader.ExtractDate(record, 8);
        if (rangeEnd.compareTo(finalEpoch) > 0) {
            throw new OrekitException(OUT_OF_RANGE_DATE, rangeEnd, startEpoch, finalEpoch);
        }

        if (tooFarRange(rangeStart, rangeEnd)) {
            // we are not interested in this record, don't parse it
            return;
        }

        // loop over chunks inside the time range
        AbsoluteDate chunkEnd = rangeStart;
        final int nbChunks    = chunks;
        final int nbCoeffs    = coeffs;
        final int first       = firstIndex;
        final double duration = chunksDuration;
        synchronized (this) {
            for (int i = 0; i < nbChunks; ++i) {

                // set up chunk validity range
                final AbsoluteDate chunkStart = chunkEnd;
                chunkEnd = (i == nbChunks - 1) ?
                        rangeEnd : new AbsoluteDate(rangeStart, (i + 1) * duration);

                // extract Chebyshev coefficients for the selected body
                // and convert them from kilometers to meters
                final double[] xCoeffs = new double[nbCoeffs];
                final double[] yCoeffs = new double[nbCoeffs];
                final double[] zCoeffs = new double[nbCoeffs];
                for (int k = 0; k < nbCoeffs; ++k) {
                    final int index = first + 3 * i * nbCoeffs + k - 1;
                    xCoeffs[k] = 1000.0 * JPLEphemeridesLoader.ExtractDouble(record, 8 * index);
                    yCoeffs[k] = 1000.0 * JPLEphemeridesLoader.ExtractDouble(record, 8 * (index +  nbCoeffs));
                    zCoeffs[k] = 1000.0 * JPLEphemeridesLoader.ExtractDouble(record, 8 * (index + 2 * nbCoeffs));
                }
                /*
                 * Build the position-velocity model for current chunk
                 */
                this.add(new PositionVelocityChebyshev(chunkStart, duration,
                                                       xCoeffs, yCoeffs, zCoeffs));

            }
        }

    }

    /** Read first header record.
     * @param input input stream
     * @param name name of the file (or zip entry)
     * @return record record where to put bytes
     * @exception OrekitException if the stream does not contain a JPL ephemeris
     * @exception IOException if a read error occurs
     */
    protected static byte[] ReadFirstRecord(final InputStream input, final String name)
        throws OrekitException, IOException
    {
        // Read first part of record, up to the ephemeris type
        final byte[] firstPart = new byte[2844];
        if (!JPLEphemeridesLoader.ReadInRecord(input, firstPart, 0)) {
            throw new OrekitException(HEADER_READ_ERROR, name);
        }

        // get the ephemeris type, deduce the record size
        int recordSize = 0;
        switch (JPLEphemeridesLoader.ExtractInt(firstPart, 2840)) {
        case 405 :
            recordSize = DE405_RECORD_SIZE;
            break;
        case 406 :
            recordSize = DE406_RECORD_SIZE;
            break;
        default :
            throw new OrekitException(NOT_JPL_EPHEMERIS, name);
        }

        // build a record with the proper size and finish read of the first complete record
        final int start = firstPart.length;
        final byte[] record = new byte[recordSize];
        System.arraycopy(firstPart, 0, record, 0, firstPart.length);
        if (!JPLEphemeridesLoader.ReadInRecord(input, record, start)) {
            throw new OrekitException(HEADER_READ_ERROR, name);
        }

        return record;

    }

    /** Read bytes into the current record array.
     * @param input input stream
     * @param record record where to put bytes
     * @param start start index where to put bytes
     * @return true if record has been filled up
     * @exception IOException if a read error occurs
     */
    protected static boolean ReadInRecord(final InputStream input,
                                        final byte[] record, final int start)
        throws IOException
    {
        int index = start;
        while (index != record.length) {
            final int n = input.read(record, index, record.length - index);
            if (n < 0) {
                return false;
            }
            index += n;
        }
        return true;
    }

    /** Extract a date from a record.
     * @param record record to parse
     * @param offset offset of the double within the record
     * @return Extracted date
     */
    protected static AbsoluteDate ExtractDate(final byte[] record, final int offset) {
        final double dt = JPLEphemeridesLoader.ExtractDouble(record, offset) * 86400;
        return new AbsoluteDate(AbsoluteDate.JULIAN_EPOCH, dt, TimeScalesFactory.getTT());
    }

    /** Extract a double from a record.
     * <p>Double numbers are stored according to IEEE 754 standard, with
     * most significant byte first.</p>
     * @param record record to parse
     * @param offset offset of the double within the record
     * @return Extracted double
     */
    protected static double ExtractDouble(final byte[] record, final int offset) {
        final long l8 = ((long) record[offset + 0]) & 0xffl;
        final long l7 = ((long) record[offset + 1]) & 0xffl;
        final long l6 = ((long) record[offset + 2]) & 0xffl;
        final long l5 = ((long) record[offset + 3]) & 0xffl;
        final long l4 = ((long) record[offset + 4]) & 0xffl;
        final long l3 = ((long) record[offset + 5]) & 0xffl;
        final long l2 = ((long) record[offset + 6]) & 0xffl;
        final long l1 = ((long) record[offset + 7]) & 0xffl;
        final long l = (l8 << 56) | (l7 << 48) | (l6 << 40) | (l5 << 32) |
                       (l4 << 24) | (l3 << 16) | (l2 <<  8) | l1;
        return Double.longBitsToDouble(l);
    }

    /** Extract an int from a record.
     * @param record record to parse
     * @param offset offset of the double within the record
     * @return Extracted int
     */
    protected static int ExtractInt(final byte[] record, final int offset) {
        final int l4 = ((int) record[offset + 0]) & 0xff;
        final int l3 = ((int) record[offset + 1]) & 0xff;
        final int l2 = ((int) record[offset + 2]) & 0xff;
        final int l1 = ((int) record[offset + 3]) & 0xff;
        return (l4 << 24) | (l3 << 16) | (l2 <<  8) | l1;
    }

    /** Extract a String from a record.
     * @param record record to parse
     * @param offset offset of the string within the record
     * @param length maximal length of the string
     * @return Extracted string, with whitespace characters stripped
     */
    protected static String ExtractString(final byte[] record, final int offset, final int length) {
        try {
            return new String(record, offset, length, "US-ASCII").trim();
        } catch (UnsupportedEncodingException uee) {
            throw OrekitException.createInternalError(uee);
        }
    }
}
